package weeny.pre.face;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;

import prefuse.data.Table;
import prefuse.data.expression.BooleanLiteral;
import prefuse.data.expression.Expression;
import prefuse.data.expression.Function;
import prefuse.data.expression.FunctionTable;
import prefuse.data.expression.Predicate;
import prefuse.data.expression.parser.ExpressionParser;
import prefuse.data.tuple.TupleSet;
import prefuse.util.DataLib;
import prefuse.visual.VisualItem;
import prefuse.visual.VisualTable;
import weeny.pre.display.Initialization;
import weeny.pre.display.MoreVisualization;
import weeny.pre.expression.SafePredicate;
import weeny.util.logs.Debug;

/**
 * Give you a JTextField to input expressions, besides the JTextField, there
 * is a combobox to show all available functions and let users choose to 
 * generate the expression. For example:
 *    a) "_visible" ("_visible" is a boolean column in visual table) can filter 
 *       out those are not visible. "NOT _visible" will do the opposite.
 *    b) "_size > 10" will give you all the tuples has "_size" bigger than 10.
 *    For more information, you should see the prefuse source to know how the
 *    they are parsed. There is a combo box telling you what functions you can 
 *    use to write the expression.
 * <p>   
 * Please notice that the combobox only lists the available functions, there
 * are operators which can also be used but not listed there, e.g.:
 *    a) numeric operators:  +, -, *, /, etc.
 *    b) and Logic operators: AND, &&, OR, ||, etc.
 *    c) and Comparison operators: <, <=, >, etc.
 * And please see {@link prefuse.data.expression.parser.ExpressionParser} 
 * <p>
 * The interface is like:
 * +----------------+----------------------------------+
 * | FUNC COMBO_BOX | INPUT_FIELD                      |
 * +----------------+----------------------------------+
 * And we have several components:
 * COMBO BOX:
 *    The COMBO_BOX lists some function for you to choose (of course, you can
 *    also directly input in the INPUT_FIELD). When you choose a function, it
 *    will be inserted into the insertion position in the INPUT_FIELD. And the
 *    the function parameters will be indicated by "?". Please replace all "?" 
 *    with anything you really need. For more details about functions, please 
 *    refer to {@link FunctionTable}. 
 * POPUP MENU:
 *    And in the INPUT_FIELD box, you can right click to invoke a pop-up menu. 
 *    The pop-up menu will list all possible group names and field names you can
 *    use to complete the expression. Choose one to insert it into the insertion
 *    position in the INPUT_FIELD.
 * TRIGGER:
 *    The action is trigger by pressing the "enter" key. Please notice that the
 *    actually expression {@link #activeExpr} is only updated when you press
 *    "enter" in the INPUT_FIELD.
 * LISTENER:
 *    You can also add {@link ActionListener} to this expressionbox. which also
 *    is called when you press the "enter" key. In the ActionListener you can
 *    use the modifiers, for example, you are holding "Shift" or "Alt" when you
 *    press "enter", that kind of information.
 * EXPRESSION:
 *    You can call {@link #getExpr()} function to get the "last entered" expr
 *    any time you want.
 * 
 * Something else:
 * 1) You can call {@link ExpressionBox#setInfoLabel(JLabel)} to assign an info
 *    label to show how many items are filtered. The number is counted when you 
 *    hit enter
 * 2) You can {@link #resetFuncList(List)}, so you can only show the functions
 *    you only need. But when you only show a fraction of {@link #ALL_ITEMS},
 *    there will be an extra option called {@link #SHOW_ALL_FUNCTION} at the end
 *    of the list. When you click it, the COMBO_BOX will switch to show all
 *    functions.
 * 3) We collect the functions from {@link FunctionTable}. Prefuse have some
 *    predefined function. We add some more in {@link Initialization}. You can
 *    add some more there too. However, please make sure ExpressionBox is inited
 *    after the FunctionTable is modified. Otherwise, {@link #ALL_ITEMS} may not
 *    contain what you add later, because we only collect {@link #ALL_ITEMS} and
 *    the related lists once.
 *     
 * @author weeny
 *
 */
public class ExpressionBox extends Box{
	public static final Dimension DEFAULT_COMBO_SIZE = new Dimension(100, 30);
	//the first item in the combobox, will be ignored
	protected static final String FUNCTION_COMBO_TITLE = "Available funcs:";
	//provide this option to you if the function list is not ALL_ITEMS
	protected static final String SHOW_ALL_FUNCTION = "(show all functions)";
	
	//several predefined function list
	public static final List<String> ALL_ITEMS    = new ArrayList<String>();
	public static final List<String> INT_ITEMS    = new ArrayList<String>();
	public static final List<String> DOUBLE_ITEMS = new ArrayList<String>();
	public static final List<String> STRING_ITEMS = new ArrayList<String>();
	public static final List<String> BOOL_ITEMS   = new ArrayList<String>();
	static {//fill all list above
		try{//since s_finctionTable is private, so we use Reflection
			Field fd = FunctionTable.class.getDeclaredField("s_functionTable");
			fd.setAccessible(true);
			HashMap table = (HashMap) fd.get(FunctionTable.class);
			Set<String> temp = new HashSet();
			for(Object key : table.keySet()){
				temp.add(key.toString().toUpperCase());
			}
			ALL_ITEMS.addAll(temp);
			Collections.sort(ALL_ITEMS);
			for (String string : temp) {
				Function func = FunctionTable.createFunction(string);
				if (func.getType(VisualItem.SCHEMA) == int.class) {
					INT_ITEMS.add(string);
				}
				if (func.getType(VisualItem.SCHEMA) == double.class) {
					DOUBLE_ITEMS.add(string);
				}
				if (func.getType(VisualItem.SCHEMA) == String.class) {
					STRING_ITEMS.add(string);
				}
				if (func.getType(VisualItem.SCHEMA) == boolean.class) {
					BOOL_ITEMS.add(string);
				}
			}
			Collections.sort(INT_ITEMS);
			Collections.sort(DOUBLE_ITEMS);
			Collections.sort(STRING_ITEMS);
			Collections.sort(BOOL_ITEMS);
		}catch (Exception e) {Debug.prob("Cannot find functions names");}
	}
	
	//information label to show something such as the parse results, etc.
	protected JLabel infoLabel = null;
	public void setInfoLabel(JLabel label){
		infoLabel = label;
	}
	protected void setText(String text){
		exprField.setText(text);
	}
	protected void updateLabelInfo(){
		if (infoLabel == null) {
			Debug.info("info label is not set, so ignore the info");
			return;
		}
		String info = " ";
		if (activeExpr == null || activeExpr instanceof Predicate){
			Predicate pred = null;
			if (activeExpr != null) {
				pred = new SafePredicate(activeExpr);
			}
			else {
				pred = BooleanLiteral.TRUE;
			}
			int cnt = DataLib.count(vis.items(pred), null);
			info += "Predicate ("+cnt+" item(s))";
		}
		else{
			info += "Not a Predicate.";
		}
		infoLabel.setText(info);
	}
	protected Expression activeExpr = null;
	protected JComboBox funcCombo = new JComboBox();
	protected JTextField exprField = new JTextField();
	protected MoreVisualization vis;
	protected JPopupMenu paraPopup = new JPopupMenu();
	//if a column or a group is chosen, it will replace the selected content in
	//the exprField
	protected ActionListener popupColumnAction = new ActionListener(){
		public void actionPerformed(ActionEvent e) {
			String column = ((JMenuItem)e.getSource()).getText();
			exprField.replaceSelection("["+column+"]");
		}
	};
	protected ActionListener popupGroupAction = new ActionListener(){
		public void actionPerformed(ActionEvent e) {
			String group = ((JMenuItem)e.getSource()).getText();
			exprField.replaceSelection("'"+group+"'");
		}
	};
	//we will call this function every time we right click in the text field
	protected void resetParaPopup(){
		//reset the popup menu to show possible parameters
		paraPopup.removeAll();
		JMenu groupMenu = new JMenu("Groups");
		JMenu columnMenu = new JMenu("Columns");
		paraPopup.add(groupMenu);
		paraPopup.add(columnMenu);
		Set<String> columnSet = new HashSet<String>();
		Set<String> groupSet = vis.getGroups();
		for (String group : groupSet) {
			JMenuItem groupItem = groupMenu.add(group);
			groupItem.addActionListener(popupGroupAction);
			TupleSet ts = vis.getVisualGroup(group);
			if (ts != null && ts instanceof VisualTable) {
				Table t = (Table) ts;
				for(int i = 0; i<t.getColumnCount(); i++) {
					columnSet.add(t.getColumnName(i));
				}
			}
		}
		List<String> columnList = new ArrayList<String>(columnSet);
		Collections.sort(columnList);
		for (String string : columnList) {
			JMenuItem columnItem = columnMenu.add(string);
			columnItem.addActionListener(popupColumnAction);
		}
	}
	/**
	 * if a function is chosen, it will be appended to the end of the exprField,
	 * then we add "?,..,?" in the parameter part, which indicates how many
	 * parameters you need to input, and selected the first "?". usually, you
	 * usually should use the popup menu to choose the parameters you need.
	 */
	protected ActionListener funcComboAction = new ActionListener() {
		public void actionPerformed(ActionEvent e) {
			String selected = funcCombo.getSelectedItem().toString();
			if (selected.equals(SHOW_ALL_FUNCTION)) {
				resetFuncList(ALL_ITEMS);
				return;
			}
			if (!ALL_ITEMS.contains(selected)) {
				Debug.info("Ignore no function item:"+selected);
				return;
			}
			Function func = FunctionTable.createFunction(selected);
			selected+="(";
			for(int i = 0; i<func.getParameterCount(); i++){
				selected+="?";
				if (i != func.getParameterCount()-1)
					selected+=",";
			}
			selected+=")";
			if (exprField.getText().trim().length() == 0)
				exprField.setText(selected);
			else{
				exprField.setText(exprField.getText()+" "+selected);
			}
			int start = exprField.getText().length();
			int end = start;
			if (func.getParameterCount() != 0){
				start = exprField.getText().indexOf("?");
				end = start+1;
			}
			exprField.setSelectionStart(start);
			exprField.setSelectionEnd(end);
		}
	};
	/**
	 * Reset the function list to show the specified list. It is useful, because
	 * the {@link #ALL_ITEMS} are actually very long.
	 * @param list
	 */
	public void resetFuncList(List<String> list){
		funcCombo.removeActionListener(funcComboAction);
		funcCombo.removeAllItems();
		funcCombo.addItem(FUNCTION_COMBO_TITLE);
		for( String item : list){
			if (ALL_ITEMS.contains(item)) {
				funcCombo.addItem(item);
			}
			else {
				Debug.prob(item+" is not a valid function");
			}
		}
		if (list != ALL_ITEMS) {
			funcCombo.addItem(SHOW_ALL_FUNCTION);
		}
		funcCombo.addActionListener(funcComboAction);
	}
	public ExpressionBox(MoreVisualization vis, String label) {
		super(BoxLayout.X_AXIS);
		this.vis = vis;
		funcCombo.addActionListener(funcComboAction);
		resetFuncList(ALL_ITEMS);
		exprField.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {
				if (e.getButton() == 3) {
					resetParaPopup();
					paraPopup.show(exprField, e.getX(), e.getY());
				}
			}
		});
		
		//monitoring enter key, if it is pressed, fire an ActionEvent with the
		//corresponding modifiers
		exprField.addKeyListener(new KeyListener() {
			public void keyTyped(KeyEvent e) {}
			public void keyReleased(KeyEvent e) {}
			public void keyPressed(KeyEvent e) {
				if (e.getKeyCode() != 10) {
					return;
				}
				int id = ActionEvent.ACTION_PERFORMED;
				int modifier = e.getModifiers();//is alt down? is shift down?
				ActionEvent ae = new ActionEvent("", id, "", modifier);
				updateActiveExpr();
				updateLabelInfo();
				fireActions(ae);
			}
		});
		
		funcCombo.setFocusable(false);
		funcCombo.setPreferredSize(DEFAULT_COMBO_SIZE);
		
		this.add(Box.createHorizontalStrut(5));
		if (label != null)
			this.add(new JLabel(label));
		this.add(funcCombo);
		this.add(exprField);
	}
	/**
	 * Resize the combo box size, by default it is {@link #DEFAULT_COMBO_SIZE},
	 * which may be not enough to show all function names. So you can change it
	 * if you want. 
	 */
	public void resizeCombo(Dimension newSize){
		funcCombo.setPreferredSize(newSize);
		revalidate();
		repaint();
	}
	public ExpressionBox(MoreVisualization vis) {
		this(vis, "Exp:");
	}
	
	public Expression getExpr(){
		return activeExpr;
	}
	
	/**
	 * Based on the user input, re-calculate the expression. Will be null if and
	 * only if: 1) the input length is 0, 2) or having error when parseing the
	 * input
	 */
	protected void updateActiveExpr(){
		if (exprField.getText().length()==0) {
			activeExpr = null;
			Debug.info("Set expression to null (length 0)");
		}
		else {
			String text = exprField.getText();
			try {
				activeExpr = ExpressionParser.parse(text, true);
			} catch (Exception ep) {
				JOptionPane.showConfirmDialog(null, ep.getMessage());
				Debug.info("Set expression to null (parse error!)");
				activeExpr = null;
			}
		}
	}
	
	private List<ActionListener> actionListeners = new ArrayList();
	/**
	 * Add/Remove actionListeners, all the listeners are invoked when you press
	 * "enter" key in the text input field
	 */
	public void addActionListener(ActionListener l){
		actionListeners.add(l);
	}
	/**
	 * Add/Remove actionListeners, all the listeners are invoked when you press
	 * "enter" key in the text input field
	 */
	public void removeActionListener(ActionListener l){
		actionListeners.remove(l);
	}
	//Update the expression and the info label. Also invoke all the  listeners
	//listening to the expressionbox
	protected void fireActions(ActionEvent e){
		Object source = e.getSource();
		e.setSource(this);
		for (ActionListener l : actionListeners){
			l.actionPerformed(e);
		}
		e.setSource(source);
	}
}
